/* * Copyright 2009 Richard Zschech. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package net.zschech.gwt.comettest.client; import java.io.Serializable; import java.util.List; import net.zschech.gwt.comet.client.CometClient; import net.zschech.gwt.comet.client.CometListener; import net.zschech.gwt.comet.client.CometSerializer; import net.zschech.gwt.comet.client.SerialMode; import net.zschech.gwt.comet.client.SerialTypes; import com.google.gwt.core.client.Duration; import com.google.gwt.core.client.EntryPoint; import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.DivElement; import com.google.gwt.dom.client.Document; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.rpc.StatusCodeException; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.HTML; import com.google.gwt.user.client.ui.RootPanel; import com.google.gwt.user.client.ui.ScrollPanel; public class CometTestEntryPoint implements EntryPoint { private CometTestServiceAsync cometTestService; private CometTest cometTest; private HTML messages; private ScrollPanel scrollPanel; private CometTest[][] tests; private int allX; private int allY; @Override public void onModuleLoad() { GWT.setUncaughtExceptionHandler(new GWT.UncaughtExceptionHandler() { @Override public void onUncaughtException(Throwable e) { output("uncaught " + string(e), "red"); e.printStackTrace(); } }); cometTestService = GWT.create(CometTestService.class); messages = new HTML(); scrollPanel = new ScrollPanel(); scrollPanel.setHeight("250px"); scrollPanel.add(messages); RootPanel.get().add(scrollPanel); tests = new CometTest[][] {{ new ReconnectionTest(true), new ReconnectionTest(false), }, { new ConnectionTest(true), new ConnectionTest(false), }, { new ErrorTest(), }, { new EscapeTest(null), new EscapeTest(SerialMode.RPC), new EscapeTest(SerialMode.DE_RPC), }, { new ThroughputTest(true, true, null), new ThroughputTest(true, true, SerialMode.RPC), new ThroughputTest(true, true, SerialMode.DE_RPC), }, { new ThroughputTest(true, false, null), new ThroughputTest(true, false, SerialMode.RPC), new ThroughputTest(true, false, SerialMode.DE_RPC), }, { new ThroughputTest(false, false, null), new ThroughputTest(false, false, SerialMode.RPC), new ThroughputTest(false, false, SerialMode.DE_RPC), }, { new LatencyTest(true, true, null), new LatencyTest(true, true, SerialMode.RPC), new LatencyTest(true, true, SerialMode.DE_RPC), }, { new LatencyTest(true, false, null), new LatencyTest(true, false, SerialMode.RPC), new LatencyTest(true, false, SerialMode.DE_RPC), }, { new LatencyTest(false, false, null), new LatencyTest(false, false, SerialMode.RPC), new LatencyTest(false, false, SerialMode.DE_RPC), }, { new OrderTest(true, true, null), new OrderTest(true, true, SerialMode.RPC), new OrderTest(true, true, SerialMode.DE_RPC), }, { new OrderTest(true, false, null), new OrderTest(true, false, SerialMode.RPC), new OrderTest(true, false, SerialMode.DE_RPC), }, { new OrderTest(false, false, null), new OrderTest(false, false, SerialMode.RPC), new OrderTest(false, false, SerialMode.DE_RPC), }, { new PaddingTest() }, { new SlowBrowserTest() }}; FlowPanel controls = new FlowPanel(); controls.add(new Button("stop", new ClickHandler() { @Override public void onClick(ClickEvent arg0) { if (cometTest != null) { cometTest.stop(); cometTest = null; } allX = -1; } })); controls.add(new Button("clear", new ClickHandler() { @Override public void onClick(ClickEvent arg0) { messages.setHTML(""); } })); controls.add(new Button("all", new ClickHandler() { @Override public void onClick(ClickEvent e) { runAll(); } })); RootPanel.get().add(controls); for (CometTest[] typeTests : tests) { controls = new FlowPanel(); for (CometTest t : typeTests) { final CometTest test = t; controls.add(new Button(test.name, new ClickHandler() { @Override public void onClick(ClickEvent e) { if (cometTest != null) { cometTest.stop(); } allX = -1; cometTest = test; test.start(); } })); } RootPanel.get().add(controls); } } private void runAll() { allX = 0; allY = 0; tests[allX][allY].start(); } private void runNext() { if (allX != -1) { allY++; if (allY >= tests[allX].length) { allX++; allY = 0; if (allX >= tests.length) { output("All done!", "lime"); allX = -1; return; } } tests[allX][allY].start(); } } public static final char ESCAPE_START = 32; public static final char ESCAPE_END = 127; public static final String ESCAPE; static { StringBuilder result = new StringBuilder(); result.append(' '); // event source discards prefixed spaces for (char i = ESCAPE_START; i <= ESCAPE_END; i++) { result.append(i); } result.append("\n\r\r\n\\/\n\t"); result.append("')</script>"); result.append(' '); ESCAPE = result.toString(); } @SerialTypes(mode = SerialMode.RPC, value = { TestData.class }) public static abstract class RPCTestCometSerializer extends CometSerializer { } @SerialTypes(mode = SerialMode.DE_RPC, value = { TestData.class }) public static abstract class DeRPCTestCometSerializer extends CometSerializer { } public static class TestData implements Serializable { private static final long serialVersionUID = 2554091659231006755L; public double d; public String s; public TestData() { } public TestData(double d, String s) { this.d = d; this.s = s; } } abstract class CometTest implements CometListener { final String name; final boolean session; CometClient cometClient; double startTime; double stopTime; double connectedTime; int connectedCount; double disconnectedTime; int disconnectedCount; int errorCount; int heartbeatCount; int refreshCount; int messageCount; int messagesCount; String failure; boolean pass; CometTest(String name, boolean session) { this.name = name + " session=" + session; this.session = session; } abstract void start(); void start(String url) { start(url, (CometSerializer) null); } void start(String url, SerialMode mode) { final CometSerializer serializer; if (mode == null) { url = url + (url.contains("?") ? "&" : "?") + "mode=string"; serializer = null; } else if (mode == SerialMode.RPC) { serializer = GWT.create(RPCTestCometSerializer.class); } else { serializer = GWT.create(DeRPCTestCometSerializer.class); } start(url + (url.contains("?") ? "&" : "?") + "session=" + session, serializer); } private void start(final String url, final CometSerializer serializer) { reset(); cometTestService.invalidateSession(new AsyncCallback<Boolean>() { @Override public void onSuccess(Boolean result) { if (session) { cometTestService.createSession(new AsyncCallback<Boolean>() { @Override public void onSuccess(Boolean result) { startTime = Duration.currentTimeMillis(); output("start " + name, "black"); doStart(url, serializer); } @Override public void onFailure(Throwable error) { output("create session failure " + string(error), "red"); } }); } else { startTime = Duration.currentTimeMillis(); output("start " + name, "black"); doStart(url, serializer); } } @Override public void onFailure(Throwable error) { output("invalidate session failure " + string(error), "red"); } }); } void reset() { startTime = 0; stopTime = 0; connectedTime = 0; connectedCount = 0; disconnectedTime = 0; disconnectedCount = 0; errorCount = 0; heartbeatCount = 0; refreshCount = 0; messageCount = 0; messagesCount = 0; pass = false; failure = null; } void doStart(String url, CometSerializer serializer) { cometClient = new CometClient(url, serializer, this); cometClient.start(); } void stop() { doStop(); cometTest = null; stopTime = Duration.currentTimeMillis(); output("stop " + name + " " + (stopTime - startTime) + "ms", "black"); if (pass && failure == null) { output("pass!", "lime"); } else { output("fail :\n" + (failure == null ? "unknown" : failure), "red"); } runNext(); } protected void doStop() { if (cometClient != null) { cometClient.stop(); cometClient = null; } } void outputStats() { output("count : " + messageCount, "black"); output("rate : " + messageCount / (disconnectedTime - connectedTime) * 1000 + "/s", "black"); output("batch size: " + (double) messageCount / (double) messagesCount, "black"); } @Override public void onConnected(int heartbeat) { connectedTime = Duration.currentTimeMillis(); connectedCount++; output("connected " + connectedCount + " " + (connectedTime - startTime) + "ms heartbeat: " + heartbeat, "silver"); assertTrue("connected once", connectedCount == 1); } @Override public void onDisconnected() { disconnectedTime = Duration.currentTimeMillis(); disconnectedCount++; output("disconnected " + disconnectedCount + " " + (disconnectedTime - connectedTime) + "ms", "silver"); assertTrue("disconnected once", disconnectedCount == 1); stop(); } @Override public void onError(Throwable exception, boolean connected) { double errorTime = Duration.currentTimeMillis(); errorCount++; output("error " + errorCount + " " + (errorTime - startTime) + "ms " + connected + " " + exception, "red"); fail(exception.toString()); stop(); } @Override public void onHeartbeat() { double heartbeatTime = Duration.currentTimeMillis(); heartbeatCount++; output("heartbeat " + heartbeatCount + " " + (heartbeatTime - connectedTime) + "ms", "silver"); } @Override public void onRefresh() { double refreshTime = Duration.currentTimeMillis(); refreshCount++; output("refresh " + refreshCount + " " + (refreshTime - connectedTime) + "ms", "silver"); } @Override public void onMessage(List<? extends Serializable> messages) { messagesCount++; messageCount += messages.size(); } void assertTrue(String message, boolean b) { if (!b) { fail(message); } else { pass = true; } } void assertEquals(String message, Object expected, Object actual) { if (!expected.equals(actual)) { fail(message + " expected " + expected + " actual " + actual); } else { pass = true; } } void pass() { pass = true; } void fail(String message) { if (failure == null) { failure = message; } else { failure += "\n" + message; } } } class ConnectionTest extends CometTest { private final int connectionTime = 120 * 1000; ConnectionTest(boolean session) { super("heartbeat and session keep alive", session); } @Override void start() { String url = GWT.getModuleBaseURL() + "connection?delay=" + connectionTime; super.start(url); } @Override void stop() { assertTrue("connection time", disconnectedTime - connectedTime >= connectionTime - 100); outputStats(); super.stop(); } } class ReconnectionTest extends CometTest { private final int connectionTime = 120 * 1000; ReconnectionTest(boolean session) { super("reconnection", session); } @Override void start() { String url = GWT.getModuleBaseURL() + "connection?delay=" + connectionTime; super.start(url); } @Override public void onConnected(int heartbeat) { connectedTime = Duration.currentTimeMillis(); connectedCount++; if (connectedCount > 1) { pass(); stop(); } else { output("connected " + connectedCount + " " + (connectedTime - startTime) + "ms heartbeat: " + heartbeat, "silver"); output("stop your server now!", "blue"); } } @Override public void onError(Throwable exception, boolean connected) { double errorTime = Duration.currentTimeMillis(); errorCount++; output("error " + errorCount + " " + (errorTime - startTime) + "ms " + connected + " " + exception, "silver"); output("start your server now!", "blue"); } } class ErrorTest extends CometTest { ErrorTest() { super("error", false); } @Override void start() { String url = GWT.getModuleBaseURL() + "error"; super.start(url); } @Override public void onError(Throwable exception, boolean connected) { double errorTime = Duration.currentTimeMillis(); errorCount++; output("error " + errorCount + " " + (errorTime - startTime) + "ms " + connected + " " + exception, "lime"); assertTrue("status code exception", exception instanceof StatusCodeException); if (exception instanceof StatusCodeException) { assertEquals("status code", 417, ((StatusCodeException) exception).getStatusCode()); assertEquals("status message", "Oh Noes!", ((StatusCodeException) exception).getEncodedResponse()); } stop(); } } class EscapeTest extends CometTest { private final SerialMode mode; EscapeTest(SerialMode mode) { super("escape mode=" + mode, false); this.mode = mode; } @Override void start() { String url = GWT.getModuleBaseURL() + "escape"; super.start(url, mode); } @Override public void onMessage(List<? extends Serializable> messages) { super.onMessage(messages); pass(); for (Serializable m : messages) { String type; String message; if (m instanceof TestData) { type = "gwt serialized object"; message = ((TestData) m).s; } else if (m instanceof String) { type = "string"; message = (String) m; } else if (m == null) { continue; } else { fail("unexpected object " + m.getClass() + " " + m); continue; } if (ESCAPE.length() != message.length()) { fail(type + " expected message length " + ESCAPE.length() + " acutal " + message.length()); } else { for (int i = 0; i < ESCAPE.length(); i++) { char expected = ESCAPE.charAt(i); char actual = message.charAt(i); if (expected != actual) { fail(type + " expected character " + expected + " 0x" + Integer.toHexString(expected) + " actual " + actual + " 0x" + Integer.toHexString(actual)); } } } } } } abstract class MessagingTest extends CometTest { private final String url; private final boolean refresh; private final SerialMode mode; private final int count; private final int batch; private final int delay; MessagingTest(String name, boolean session, boolean refresh, SerialMode mode, int count, int batch, int delay) { super(name + " refresh=" + refresh + " mode=" + mode, session); this.url = name; this.refresh = refresh; this.mode = mode; this.count = count; this.batch = batch; this.delay = delay; } @Override void start() { String url = GWT.getModuleBaseURL() + this.url + "?count=" + count + "&batch=" + batch; if (mode == null) { url += "&mode=string"; } if (!refresh) { url += "&length=" + (count * batch * 10000); } url += "&delay=" + delay; super.start(url, mode); } @Override void stop() { assertTrue("count", count * batch == messageCount); outputStats(); super.stop(); } } class ThroughputTest extends MessagingTest { ThroughputTest(boolean session, boolean refresh, SerialMode mode) { super("throughput", session, refresh, mode, 1000, 10, 0); } } class LatencyTest extends MessagingTest { private double latency; private double min = Double.MAX_VALUE; private double max = Double.MIN_VALUE; LatencyTest(boolean session, boolean refresh, SerialMode mode) { super("latency", session, refresh, mode, 1000, 1, 10); } @Override void reset() { super.reset(); latency = 0; min = Double.MAX_VALUE; max = Double.MIN_VALUE; } @Override public void onMessage(List<? extends Serializable> messages) { super.onMessage(messages); double now = Duration.currentTimeMillis(); for (Serializable m : messages) { double message; if (m instanceof TestData) { message = ((TestData) m).d; } else if (m instanceof String) { message = Double.parseDouble((String) m); } else { continue; } double messageLatency = now - message; latency += messageLatency; if (messageLatency < min) { min = messageLatency; } if (messageLatency > max) { max = messageLatency; } if (messageLatency > 250) { output("latency " + messageLatency, "red"); } } } @Override void outputStats() { super.outputStats(); output("latency : " + latency / messageCount + "ms", "black"); output("min : " + min + "ms", "black"); output("max : " + max + "ms", "black"); } } class OrderTest extends MessagingTest { OrderTest(boolean session, boolean refresh, SerialMode mode) { super("order", session, refresh, mode, 1000, 1, 0); } @Override public void onMessage(List<? extends Serializable> messages) { int count = messageCount; super.onMessage(messages); for (Serializable m : messages) { double message; if (m instanceof TestData) { message = ((TestData) m).d; } else if (m instanceof String) { message = Double.parseDouble((String) m); } else { continue; } assertTrue("expected count " + count + " actual " + message, count == message); count++; } } } class PaddingTest extends CometTest { int min; int max; int padding; PaddingTest() { super("padding", false); } @Override void start() { doTest(0, 8 * 1024); } private void doTest(int min, int max) { if (min == max) { output("padding required: " + min, "lime"); pass(); stop(); } else { this.min = min; this.max = max; this.padding = (min + max) / 2; output("padding test: " + min + " " + max + " " + padding, "silver"); String url = GWT.getModuleBaseURL() + "connection?delay=60000&padding=" + padding; doStart(url, null); } } @Override public void onConnected(int heartbeat) { output("connected", "silver"); doStop(); doTest(min, padding - 1); } @Override public void onDisconnected() { output("disconnected", "silver"); } @Override public void onError(Throwable exception, boolean connected) { output("error " + exception.toString(), "silver"); doStop(); doTest(padding + 1, max); } } class SlowBrowserTest extends MessagingTest { SlowBrowserTest() { super("slowbrowser", true, true, null, 12000, 1, 0); } @Override public void onMessage(List<? extends Serializable> messages) { super.onMessage(messages); int waitTime = messages.size() * 10; double time = Duration.currentTimeMillis() + waitTime; while (Duration.currentTimeMillis() < time) { } output("waited " + waitTime + "ms " + messages.size() + "messages", "black"); } } private static String string(Throwable exception) { StringBuilder result = new StringBuilder(exception.toString()); exception = exception.getCause(); while (exception != null) { result.append("\n").append(exception.toString()); exception = exception.getCause(); } return result.toString(); } public void output(String text, String color) { DivElement div = Document.get().createDivElement(); div.setInnerText(text); div.setAttribute("style", "font-family:monospace;white-space:pre;color:" + color); messages.getElement().appendChild(div); scrollPanel.scrollToBottom(); } }